#!/usr/bin/env python3
"termux-create-package: Utility to create .deb files."

import argparse
import io
import json
import os
import subprocess
import sys
import tarfile
import tempfile

DESCRIPTION = """Create a Termux package from a JSON manifest file. Example of manifest:

{
  "name": "mypackage",
  "version": "0.1",
  "arch": "all",
  "maintainer": "@MyGithubNick",
  "description": "This is a hello world package",
  "homepage": "https://example.com",
  "depends": ["python"],
  "recommends": ["vim"],
  "suggests": ["vim-python"],
  "provides": ["vi"],
  "conflicts": ["vim-python-git"],
  "files" : {
    "hello-world.py": "bin/hello-world",
    "hello-world.1": "share/man/man1/hello-world.1"
  }
}

The "maintainer", "description", "homepage", "depends", "recommends", "suggests", "provides" and "conflicts" fields are all optional.

The "depends", "recommends", and "suggests" fields should be a comma-separated list of packages that this package may depends on. Unlike "suggests", "depends" and "recommends" will be installed automatically when this package is installed using apt.

The "arch" field defaults to "all" (that is, a platform-independent package not containing native code) and can be any of arm/i686/aarch64/x86_64.  Run "uname -m" to find out arch name if building native code inside Termux.

The "files" map is keyed from paths to files to include (relative to the current directory) while the values contains the paths where the files should end up after installation (relative to $PREFIX).

Optional debscripts named "preinst", "postinst", "prerm", and "postrm" will be automatically included and executed upon installation and removing. They should exist within the same directory as the manifest.

The resulting .deb file can be installed by Termux users with:
  apt install ./package-file.deb
or by hosting it in an apt repository using the termux-apt-repo tool."""

def set_default_manifest_values(manifest):
    "Setup default values in a package manifest."
    properties = {"arch": "all",
                  "depends": [],
                  "recommends": [],
                  "suggests": [],
                  "provides": [],
                  "conflicts": [],
                  "maintainer": "None",
                  "description": "No description"}
    for property, value in properties.items():
        if property not in manifest:
            manifest[property] = value

def validate_manifest(manifest):
    "Validate that the package manifest makes sense."
    for property in ("name", "version", "files"):
        if property not in manifest:
            sys.exit(f"Missing mandatory {property} property")
    if manifest["arch"] not in ("all", "arm", "i686", "aarch64", "x86_64"):
        sys.exit('Invalid "arch" - must be one of all/arm/i686/aarch64/x86_64')

def write_control_tar(tar_path, manifest, debscripts):
    "Create a data.tar.xz from the specified manifest."
    contents = (f"Package: {manifest['name']}\n"
                f"Version: {manifest['version']}\n"
                f"Architecture: {manifest['arch']}\n"
                f"Maintainer: {manifest['maintainer']}\n"
                f"Description: {manifest['description']}\n")
    if "homepage" in manifest:
        contents += f"Homepage: {manifest['homepage']}\n"
    for property in ("depends", "recommends", "suggests", "provides", "conflicts"):
        if manifest[property]:
            contents += f"{property.capitalize()}: {','.join(manifest[property])}\n"
    control_file = io.BytesIO(contents.encode("utf8"))
    control_file.seek(0, os.SEEK_END)
    control_file_size = control_file.tell()
    control_file.seek(0)
    tarfile_info = tarfile.TarInfo(name="./control")
    tarfile_info.size = control_file_size
    with tarfile.open(tar_path, mode="w:xz", format=tarfile.GNU_FORMAT) as control_tarfile:
        control_tarfile.addfile(tarinfo=tarfile_info, fileobj=control_file)
        for debscript in debscripts:
            if os.path.isfile(debscript): control_tarfile.add(debscript)

def write_data_tar(tar_path, installation_prefix, package_files):
    "Create a data.tar.xz from the specified package files."
    with tarfile.open(tar_path, mode="w:xz", format=tarfile.GNU_FORMAT) as data_tarfile:
        for input_file_path, output_file_path in package_files.items():
            output_file_path = os.path.join(installation_prefix, output_file_path)
            data_tarfile.add(input_file_path, arcname=output_file_path, recursive=True)

def create_debfile(debfile_output, directory):
    "Create a debfile from a directory containing control and data tar files."
    subprocess.check_call(["ar", 'r', debfile_output,
                           f"{directory}/debian-binary",
                           f"{directory}/control.tar.xz",
                           f"{directory}/data.tar.xz"])

if __name__ == "__main__":
    "Generate a deb file from a JSON manifest."
    installation_prefix = "/data/data/com.termux/files/usr/"
    argument_parser = argparse.ArgumentParser(description=DESCRIPTION,
                                              formatter_class=argparse.RawTextHelpFormatter)
    argument_parser.add_argument("manifest", help="A JSON manifest file describing the package")
    argument_parser.add_argument("--prefix", help=f"Set prefix dir (default: {installation_prefix})")
    arguments = argument_parser.parse_args(sys.argv[1:])
    if arguments.prefix:
        installation_prefix = str(arguments.prefix)
    manifest_file_path = arguments.manifest
    manifest_dir_path = os.path.dirname(manifest_file_path)
    debscripts = [(os.path.join(manifest_dir_path, debscript))
                  for debscript in ("preinst", "postinst", "prerm", "postrm")]
    with open(manifest_file_path, 'r') as manifest_file:
        manifest = json.load(manifest_file)
    set_default_manifest_values(manifest)
    validate_manifest(manifest)
    package_name = manifest["name"]
    package_version = manifest["version"]
    package_files = manifest["files"]
    output_debfile_name = f"{package_name}_{package_version}_{manifest['arch']}.deb"
    print(f"Building {output_debfile_name}")
    package_tmp_directory = tempfile.TemporaryDirectory()
    with open(f"{package_tmp_directory.name}/debian-binary", 'w') as debian_binary:
        debian_binary.write("2.0\n")
    write_control_tar(f"{package_tmp_directory.name}/control.tar.xz", manifest, debscripts)
    write_data_tar(f"{package_tmp_directory.name}/data.tar.xz", installation_prefix, package_files)
    create_debfile(output_debfile_name, package_tmp_directory.name)
